import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import ComponentFlow
import PhotoResources
import DirectMediaImageCache
import TelegramStringFormatting
import TooltipUI

private enum SelectionTransition {
    case begin
    case change
    case end
}

private final class MediaPreviewView: SimpleLayer {
    private let context: AccountContext
    private let message: EngineMessage
    private let media: EngineMedia
    private let imageCache: DirectMediaImageCache

    private var requestedImage: Bool = false
    private var disposable: Disposable?

    init(context: AccountContext, message: EngineMessage, media: EngineMedia, imageCache: DirectMediaImageCache) {
        self.context = context
        self.message = message
        self.media = media
        self.imageCache = imageCache

        super.init()

        self.contentsGravity = .resize
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        self.disposable?.dispose()
    }

    func updateLayout(size: CGSize, synchronousLoads: Bool) {
        let processImage: (UIImage) -> UIImage = { image in
            return generateImage(size, rotatedContext: { size, context in
                context.clear(CGRect(origin: CGPoint(), size: size))
                context.addEllipse(in: CGRect(origin: CGPoint(), size: size))
                context.clip()

                UIGraphicsPushContext(context)
                image.draw(in: CGRect(origin: CGPoint(), size: size))
                UIGraphicsPopContext()
            })!
        }

        if !self.requestedImage {
            self.requestedImage = true
            if let result = self.imageCache.getImage(message: self.message._asMessage(), media: self.media._asMedia(), width: 100, possibleWidths: [100], synchronous: false) {
                if let image = result.image {
                    self.contents = processImage(image).cgImage
                }
                if let signal = result.loadSignal {
                    self.disposable = (signal
                    |> map { image in
                        return image.flatMap(processImage)
                    }
                    |> deliverOnMainQueue).startStrict(next: { [weak self] image in
                        guard let strongSelf = self else {
                            return
                        }
                        if let image = image {
                            if strongSelf.contents != nil {
                                let tempView = SimpleLayer()
                                tempView.contents = strongSelf.contents
                                tempView.frame = strongSelf.bounds
                                tempView.contentsGravity = strongSelf.contentsGravity
                                strongSelf.addSublayer(tempView)
                                tempView.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in
                                    tempView?.removeFromSuperlayer()
                                })
                            }
                            strongSelf.contents = image.cgImage
                        }
                    }).strict()
                }
            }
        }
    }
}

private func normalizeDayIndex(index: Int) -> Int {
    switch index {
    case 1:
        return 6
    case 2:
        return 0
    case 3:
        return 1
    case 4:
        return 2
    case 5:
        return 3
    case 6:
        return 4
    case 7:
        return 5
    default:
        preconditionFailure()
    }
}

private func gridDayOffset(firstDayOfWeek: Int, firstWeekdayOfMonth: Int) -> Int {
    let monthStartsWithDay = normalizeDayIndex(index: firstWeekdayOfMonth)
    let weekStartsWithDay = normalizeDayIndex(index: firstDayOfWeek)

    return (monthStartsWithDay - weekStartsWithDay + 7) % 7
}

private func gridDayName(index: Int, firstDayOfWeek: Int, strings: PresentationStrings) -> String {
    let adjustedIndex = (index + firstDayOfWeek) % 7
    switch adjustedIndex {
    case 1:
        return strings.Calendar_ShortSun
    case 2:
        return strings.Calendar_ShortMon
    case 3:
        return strings.Calendar_ShortTue
    case 4:
        return strings.Calendar_ShortWed
    case 5:
        return strings.Calendar_ShortThu
    case 6:
        return strings.Calendar_ShortFri
    case 0:
        return strings.Calendar_ShortSat
    default:
        return ""
    }
}

private class Scroller: UIScrollView {
    override init(frame: CGRect) {
        super.init(frame: frame)

        if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
            self.contentInsetAdjustmentBehavior = .never
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func touchesShouldCancel(in view: UIView) -> Bool {
        return true
    }

    @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
}

private final class ImageCache: Equatable {
    static func ==(lhs: ImageCache, rhs: ImageCache) -> Bool {
        return lhs === rhs
    }

    private struct FilledCircle: Hashable {
        var diameter: CGFloat
        var innerDiameter: CGFloat?
        var color: UInt32
    }

    private struct Text: Hashable {
        var fontSize: CGFloat
        var isSemibold: Bool
        var color: UInt32
        var string: String
    }

    private struct MonthSelection: Hashable {
        var leftRadius: CGFloat
        var rightRadius: CGFloat
        var maxRadius: CGFloat
        var color: UInt32
    }

    private var items: [AnyHashable: UIImage] = [:]

    func filledCircle(diameter: CGFloat, innerDiameter: CGFloat?, color: UIColor) -> UIImage {
        let key = AnyHashable(FilledCircle(diameter: diameter, innerDiameter: innerDiameter, color: color.argb))
        if let image = self.items[key] {
            return image
        }
        let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
            context.clear(CGRect(origin: CGPoint(), size: size))

            context.setFillColor(color.cgColor)

            context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))

            if let innerDiameter = innerDiameter {
                context.setBlendMode(.copy)
                context.setFillColor(UIColor.clear.cgColor)
                context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) / 2.0, y: (size.height - innerDiameter) / 2.0), size: CGSize(width: innerDiameter, height: innerDiameter)))
            }
        })!.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2)
        self.items[key] = image
        return image
    }

    func text(fontSize: CGFloat, isSemibold: Bool, color: UIColor, string: String) -> UIImage {
        let key = AnyHashable(Text(fontSize: fontSize, isSemibold: isSemibold, color: color.argb, string: string))
        if let image = self.items[key] {
            return image
        }

        let font: UIFont
        if isSemibold {
            font = Font.semibold(fontSize)
        } else {
            font = Font.regular(fontSize)
        }
        let attributedString = NSAttributedString(string: string, font: font, textColor: color)
        var rect = attributedString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), options: .usesLineFragmentOrigin, context: nil)
        if string == "1" {
            rect.origin.x -= 1.0
        }
        let image = generateImage(CGSize(width: ceil(rect.width), height: ceil(rect.height)), rotatedContext: { size, context in
            context.clear(CGRect(origin: CGPoint(), size: size))

            UIGraphicsPushContext(context)
            attributedString.draw(in: rect)
            UIGraphicsPopContext()
        })!
        self.items[key] = image
        return image
    }

    func monthSelection(leftRadius: CGFloat, rightRadius: CGFloat, maxRadius: CGFloat, color: UIColor) -> UIImage {
        let key = AnyHashable(MonthSelection(leftRadius: leftRadius, rightRadius: rightRadius, maxRadius: maxRadius, color: color.argb))
        if let image = self.items[key] {
            return image
        }

        let image = generateImage(CGSize(width: maxRadius, height: maxRadius), rotatedContext: { size, context in
            context.clear(CGRect(origin: CGPoint(), size: size))
            context.setFillColor(color.cgColor)

            UIGraphicsPushContext(context)

            context.clip(to: CGRect(origin: CGPoint(), size: CGSize(width: size.width / 2.0, height: size.height)))
            UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: leftRadius).fill()

            context.resetClip()
            context.clip(to: CGRect(origin: CGPoint(x: size.width / 2.0, y: 0.0), size: CGSize(width: size.width - size.width / 2.0, height: size.height)))
            UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: rightRadius).fill()

            UIGraphicsPopContext()
        })!.stretchableImage(withLeftCapWidth: Int(maxRadius / 2.0), topCapHeight: Int(maxRadius / 2.0))
        self.items[key] = image
        return image
    }
}

private final class DayEnvironment: Equatable {
    let imageCache: ImageCache
    let directImageCache: DirectMediaImageCache
    var selectionDelayCoordination: Int = 0

    init(imageCache: ImageCache, directImageCache: DirectMediaImageCache) {
        self.imageCache = imageCache
        self.directImageCache = directImageCache
    }

    static func ==(lhs: DayEnvironment, rhs: DayEnvironment) -> Bool {
        return lhs === rhs
    }
}

private final class DayComponent: Component {
    typealias EnvironmentType = DayEnvironment

    enum DaySelection {
        case none
        case edge
        case middle
    }

    let title: String
    let isCurrent: Bool
    let isEnabled: Bool
    let theme: PresentationTheme
    let context: AccountContext
    let timestamp: Int32
    let media: DayMedia?
    let selection: DaySelection
    let isSelecting: Bool
    let action: () -> Void

    init(
        title: String,
        isCurrent: Bool,
        isEnabled: Bool,
        theme: PresentationTheme,
        context: AccountContext,
        timestamp: Int32,
        media: DayMedia?,
        selection: DaySelection,
        isSelecting: Bool,
        action: @escaping () -> Void
    ) {
        self.title = title
        self.isCurrent = isCurrent
        self.isEnabled = isEnabled
        self.theme = theme
        self.context = context
        self.timestamp = timestamp
        self.media = media
        self.selection = selection
        self.isSelecting = isSelecting
        self.action = action
    }

    static func ==(lhs: DayComponent, rhs: DayComponent) -> Bool {
        if lhs.title != rhs.title {
            return false
        }
        if lhs.isCurrent != rhs.isCurrent {
            return false
        }
        if lhs.isEnabled != rhs.isEnabled {
            return false
        }
        if lhs.theme !== rhs.theme {
            return false
        }
        if lhs.context !== rhs.context {
            return false
        }
        if lhs.media != rhs.media {
            return false
        }
        if lhs.timestamp != rhs.timestamp {
            return false
        }
        if lhs.selection != rhs.selection {
            return false
        }
        if lhs.isSelecting != rhs.isSelecting {
            return false
        }
        return true
    }

    final class View: HighlightTrackingButton {
        private let highlightView: SimpleLayer
        private var selectionView: SimpleLayer?
        private let titleView: SimpleLayer
        private var mediaPreviewView: MediaPreviewView?

        private var action: (() -> Void)?
        private var currentMedia: DayMedia?
        private var currentSelection: DaySelection?

        private(set) var timestamp: Int32?
        private(set) var index: EngineMessage.Index?
        private var isHighlightingEnabled: Bool = false

        init() {
            self.highlightView = SimpleLayer()
            self.titleView = SimpleLayer()

            super.init(frame: CGRect())

            self.layer.addSublayer(self.highlightView)
            self.layer.addSublayer(self.titleView)

            self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
            self.highligthedChanged = { [weak self] highligthed in
                guard let strongSelf = self, let mediaPreviewView = strongSelf.mediaPreviewView else {
                    return
                }
                if strongSelf.isHighlightingEnabled && highligthed {
                    mediaPreviewView.opacity = 0.8
                } else {
                    let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
                    transition.updateAlpha(layer: mediaPreviewView, alpha: 1.0)
                }
            }
        }

        required init?(coder aDecoder: NSCoder) {
            preconditionFailure()
        }

        @objc private func pressed() {
            self.action?()
        }

        func update(component: DayComponent, availableSize: CGSize, environment: Environment<DayEnvironment>, transition: ComponentTransition) -> CGSize {
            let isFirstTime = self.action == nil

            self.action = component.action
            self.timestamp = component.timestamp
            self.index = component.media?.message.index
            self.isHighlightingEnabled = component.isEnabled && component.media != nil && !component.isSelecting
            
            let previousSelection = self.currentSelection ?? component.selection
            let previousSelected = previousSelection != .none
            let isSelected = component.selection != .none
            self.currentSelection = component.selection

            let diameter = min(availableSize.width, availableSize.height)
            let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: floor((availableSize.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))

            let dayEnvironment = environment[DayEnvironment.self].value
            if component.media != nil {
                self.highlightView.contents = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: nil, color: UIColor(white: 0.0, alpha: 0.2)).cgImage
            } else {
                self.highlightView.contents = nil
            }

            var animateTitle = false
            var animateMediaIn = false
            if self.currentMedia != component.media {
                self.currentMedia = component.media

                if let mediaPreviewView = self.mediaPreviewView {
                    self.mediaPreviewView = nil
                    mediaPreviewView.removeFromSuperlayer()
                } else {
                    animateMediaIn = !isFirstTime
                }

                if let media = component.media {
                    let mediaPreviewView = MediaPreviewView(context: component.context, message: media.message, media: media.media, imageCache: dayEnvironment.directImageCache)
                    self.mediaPreviewView = mediaPreviewView
                    self.layer.insertSublayer(mediaPreviewView, below: self.highlightView)
                }
            }

            let titleColor: UIColor
            let titleFontSize: CGFloat
            let titleFontIsSemibold: Bool
            if component.media != nil {
                if component.theme.overallDarkAppearance {
                    titleColor = component.theme.list.itemPrimaryTextColor
                } else {
                    titleColor = component.theme.list.itemCheckColors.foregroundColor
                }
                titleFontSize = 17.0
                titleFontIsSemibold = true
            } else {
                titleFontSize = 17.0
                switch component.selection {
                case .middle, .edge:
                    titleFontIsSemibold = true
                default:
                    titleFontIsSemibold = component.isCurrent
                }

                if case .edge = component.selection {
                    if component.theme.overallDarkAppearance {
                        titleColor = component.theme.list.itemPrimaryTextColor
                    } else {
                        titleColor = component.theme.list.itemCheckColors.foregroundColor
                    }
                } else {
                    if component.isCurrent {
                        titleColor = component.theme.list.itemAccentColor
                    } else if component.isEnabled {
                        titleColor = component.theme.list.itemPrimaryTextColor
                    } else {
                        titleColor = component.theme.list.itemDisabledTextColor
                    }
                }
            }

            switch component.selection {
            case .edge:
                let selectionView: SimpleLayer
                if let current = self.selectionView {
                    selectionView = current
                } else {
                    selectionView = SimpleLayer()
                    self.selectionView = selectionView
                    self.layer.insertSublayer(selectionView, below: self.titleView)
                }
                selectionView.frame = contentFrame
                if self.mediaPreviewView != nil {
                    selectionView.contents = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: diameter - 2.0 * 2.0, color: component.theme.list.itemCheckColors.fillColor).cgImage
                } else {
                    selectionView.contents = dayEnvironment.imageCache.filledCircle(diameter: diameter, innerDiameter: nil, color: component.theme.list.itemCheckColors.fillColor).cgImage
                }
            case .middle, .none:
                if let selectionView = self.selectionView {
                    self.selectionView = nil
                    if let _ = transition.userData(SelectionTransition.self), previousSelected != isSelected {
                        selectionView.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak selectionView] _ in
                            selectionView?.removeFromSuperlayer()
                        })
                    } else {
                        selectionView.removeFromSuperlayer()
                    }
                }
            }

            let minimizedContentScale: CGFloat = (contentFrame.width - 8.0) / contentFrame.width
            let contentScale: CGFloat
            switch component.selection {
            case .edge, .middle:
                contentScale = minimizedContentScale
            case .none:
                contentScale = 1.0
            }

            let titleImage = dayEnvironment.imageCache.text(fontSize: titleFontSize, isSemibold: titleFontIsSemibold, color: titleColor, string: component.title)
            if animateMediaIn {
                animateTitle = true
            }
            
            self.highlightView.bounds = CGRect(origin: CGPoint(), size: contentFrame.size)
            self.highlightView.position = CGPoint(x: contentFrame.midX, y: contentFrame.midY)

            if let mediaPreviewView = self.mediaPreviewView {
                mediaPreviewView.bounds = CGRect(origin: CGPoint(), size: contentFrame.size)
                mediaPreviewView.position = CGPoint(x: contentFrame.midX, y: contentFrame.midY)
                mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false)

                mediaPreviewView.transform = CATransform3DMakeScale(contentScale, contentScale, 1.0)

                if animateMediaIn {
                    mediaPreviewView.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                    self.highlightView.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                }
            }
            
            self.highlightView.transform = CATransform3DMakeScale(contentScale, contentScale, 1.0)
            
            if let _ = transition.userData(SelectionTransition.self), previousSelected != isSelected {
                if self.mediaPreviewView == nil {
                    animateTitle = true
                }
                if isSelected {
                    if component.selection == .edge {
                        let scaleIn = self.layer.makeAnimation(from: 1.0 as NSNumber, to: 0.75 as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.1)
                        let scaleOut = self.layer.springAnimation(from: 0.75 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
                        self.layer.animateGroup([scaleIn, scaleOut], key: "selection")
                        if let selectionView = self.selectionView {
                            if self.mediaPreviewView != nil {
                                let shapeLayer = CAShapeLayer()
                                let lineWidth: CGFloat = 2.0
                                shapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: diameter / 2.0 - lineWidth / 2.0, startAngle: -CGFloat.pi / 2.0, endAngle: 2 * CGFloat.pi - CGFloat.pi / 2.0, clockwise: true).cgPath
                                shapeLayer.frame = selectionView.frame
                                shapeLayer.strokeColor = component.theme.list.itemCheckColors.fillColor.cgColor
                                shapeLayer.fillColor = UIColor.clear.cgColor
                                shapeLayer.lineWidth = lineWidth
                                shapeLayer.lineCap = .round
                                selectionView.isHidden = true
                                self.layer.insertSublayer(shapeLayer, above: selectionView)
                                shapeLayer.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.25, delay: 0.1, completion: { [weak selectionView, weak shapeLayer] _ in
                                    shapeLayer?.removeFromSuperlayer()
                                    selectionView?.isHidden = false
                                })
                            } else {
                                selectionView.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
                            }
                        }
                    } else {
                        if let mediaPreviewView = self.mediaPreviewView {
                            mediaPreviewView.animateScale(from: 1.0, to: contentScale, duration: 0.2)
                        }
                        self.highlightView.animateScale(from: 1.0, to: contentScale, duration: 0.2)
                    }
                } else {
                    if let mediaPreviewView = self.mediaPreviewView {
                        mediaPreviewView.animateScale(from: minimizedContentScale, to: contentScale, duration: 0.2)
                    }
                    self.highlightView.animateScale(from: minimizedContentScale, to: contentScale, duration: 0.2)
                }
            }
            
            if animateTitle {
                let previousTitleView = SimpleLayer()
                previousTitleView.contents = self.titleView.contents
                previousTitleView.frame = self.titleView.frame
                self.titleView.superlayer?.insertSublayer(previousTitleView, above: self.titleView)
                previousTitleView.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousTitleView] _ in
                    previousTitleView?.removeFromSuperlayer()
                })
                self.titleView.animateAlpha(from: 0.0, to: 1.0, duration: 0.16)
            }
            
            self.titleView.contents = titleImage.cgImage
            let titleSize = titleImage.size

            self.highlightView.frame = CGRect(origin: CGPoint(x: contentFrame.midX - contentFrame.width * contentScale / 2.0, y: contentFrame.midY - contentFrame.width * contentScale / 2.0), size: CGSize(width: contentFrame.width * contentScale, height: contentFrame.height * contentScale))

            self.titleView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize)

            return availableSize
        }
    }

    func makeView() -> View {
        return View()
    }

    func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DayEnvironment>, transition: ComponentTransition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
    }
}

private final class MonthComponent: CombinedComponent {
    typealias EnvironmentType = DayEnvironment

    let context: AccountContext
    let model: MonthModel
    let foregroundColor: UIColor
    let strings: PresentationStrings
    let theme: PresentationTheme
    let dayAction: (Int32) -> Void
    let monthAction: (ClosedRange<Int32>) -> Void
    let selectedDays: ClosedRange<Int32>?

    init(
        context: AccountContext,
        model: MonthModel,
        foregroundColor: UIColor,
        strings: PresentationStrings,
        theme: PresentationTheme,
        dayAction: @escaping (Int32) -> Void,
        monthAction: @escaping (ClosedRange<Int32>) -> Void,
        selectedDays: ClosedRange<Int32>?
    ) {
        self.context = context
        self.model = model
        self.foregroundColor = foregroundColor
        self.strings = strings
        self.theme = theme
        self.dayAction = dayAction
        self.monthAction = monthAction
        self.selectedDays = selectedDays
    }

    static func ==(lhs: MonthComponent, rhs: MonthComponent) -> Bool {
        if lhs.context !== rhs.context {
            return false
        }
        if lhs.model != rhs.model {
            return false
        }
        if lhs.foregroundColor != rhs.foregroundColor {
            return false
        }
        if lhs.strings !== rhs.strings {
            return false
        }
        if lhs.theme !== rhs.theme {
            return false
        }
        if lhs.selectedDays != rhs.selectedDays {
            return false
        }
        return true
    }

    static var body: Body {
        let title = Child(Text.self)
        let weekdayTitles = ChildMap(environment: Empty.self, keyedBy: Int.self)
        let days = ChildMap(environment: DayEnvironment.self, keyedBy: Int.self)
        let selections = ChildMap(environment: Empty.self, keyedBy: Int.self)

        return { context in
            let sideInset: CGFloat = 14.0
            let titleWeekdaysSpacing: CGFloat = 18.0
            let weekdayDaySpacing: CGFloat = 14.0
            let weekdaySize: CGFloat = 46.0
            let weekdaySpacing: CGFloat = 6.0

            let usableWeekdayWidth = floor((context.availableSize.width - sideInset * 2.0 - weekdaySpacing * 6.0) / 7.0)
            let weekdayWidth = floor((context.availableSize.width - sideInset * 2.0) / 7.0)

            let monthName = stringForMonth(strings: context.component.strings, month: Int32(context.component.model.index - 1), ofYear: Int32(context.component.model.year - 1900))

            let title = title.update(
                component: Text(
                    text: monthName,
                    font: Font.semibold(17.0),
                    color: context.component.foregroundColor
                ),
                availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0),
                transition: .immediate
            )

            let updatedWeekdayTitles = (0 ..< 7).map { index in
                return weekdayTitles[index].update(
                    component: AnyComponent(Text(
                        text: gridDayName(index: index, firstDayOfWeek: context.component.model.firstWeekday, strings: context.component.strings),
                        font: Font.regular(10.0),
                        color: context.component.foregroundColor
                    )),
                    availableSize: CGSize(width: 100.0, height: 100.0),
                    transition: .immediate
                )
            }

            let updatedDays = (0 ..< context.component.model.numberOfDays).map { index -> _UpdatedChildComponent in
                let dayOfMonth = index + 1
                let isCurrent = context.component.model.currentYear == context.component.model.year && context.component.model.currentMonth == context.component.model.index && context.component.model.currentDayOfMonth == dayOfMonth
                var isEnabled = true
                if context.component.model.currentYear == context.component.model.year {
                    if context.component.model.currentMonth == context.component.model.index {
                        if dayOfMonth > context.component.model.currentDayOfMonth {
                            isEnabled = false
                        }
                    } else if context.component.model.index > context.component.model.currentMonth {
                        isEnabled = false
                    }
                } else if context.component.model.year > context.component.model.currentYear {
                    isEnabled = false
                }

                let dayTimestamp = Int32(context.component.model.firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(index)
                let dayAction = context.component.dayAction

                let daySelection: DayComponent.DaySelection
                if let selectedDays = context.component.selectedDays, selectedDays.contains(dayTimestamp) {
                    if selectedDays.lowerBound == dayTimestamp || selectedDays.upperBound == dayTimestamp {
                        daySelection = .edge
                    } else {
                        daySelection = .middle
                    }
                } else {
                    daySelection = .none
                }

                return days[index].update(
                    component: AnyComponent(DayComponent(
                        title: "\(dayOfMonth)",
                        isCurrent: isCurrent,
                        isEnabled: isEnabled,
                        theme: context.component.theme,
                        context: context.component.context,
                        timestamp: dayTimestamp,
                        media: context.component.model.mediaByDay[index],
                        selection: daySelection,
                        isSelecting: context.component.selectedDays != nil,
                        action: {
                            if isEnabled {
                                dayAction(dayTimestamp)
                            }
                        }
                    )),
                    environment: {
                        context.environment[DayEnvironment.self]
                    },
                    availableSize: CGSize(width: usableWeekdayWidth, height: weekdaySize),
                    transition: context.transition
                )
            }

            let titleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - title.size.width) / 2.0), y: 0.0), size: title.size)

            let monthAction = context.component.monthAction
            let firstDayStart = Int32(context.component.model.firstDay.timeIntervalSince1970)
            let lastDayStart = firstDayStart + 24 * 60 * 60 * Int32(context.component.model.numberOfDays - 1)
            
            context.add(title
                .position(CGPoint(x: titleFrame.midX, y: titleFrame.midY))
                .gesture(.tap {
                    monthAction(firstDayStart ... lastDayStart)
                })
            )

            let baseWeekdayTitleY = titleFrame.maxY + titleWeekdaysSpacing
            var maxWeekdayY = baseWeekdayTitleY

            for i in 0 ..< updatedWeekdayTitles.count {
                let weekdaySize = updatedWeekdayTitles[i].size
                let weekdayFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * weekdayWidth + floor((weekdayWidth - weekdaySize.width) / 2.0), y: baseWeekdayTitleY), size: weekdaySize)
                maxWeekdayY = max(maxWeekdayY, weekdayFrame.maxY)
                context.add(updatedWeekdayTitles[i]
                    .position(CGPoint(x: weekdayFrame.midX, y: weekdayFrame.midY))
                )
            }

            let baseDayY = maxWeekdayY + weekdayDaySpacing
            var maxDayY = baseDayY

            struct LineSelection {
                var range: ClosedRange<Int>
                var leftTimestamp: Int32
                var rightTimestamp: Int32
            }

            var selectionsByLine: [Int: LineSelection] = [:]

            for i in 0 ..< updatedDays.count {
                let gridIndex = gridDayOffset(firstDayOfWeek: context.component.model.firstWeekday, firstWeekdayOfMonth: context.component.model.firstDayWeekday) + i
                let rowIndex = gridIndex % 7
                let lineIndex = gridIndex / 7

                if let selectedDays = context.component.selectedDays {
                    let dayTimestamp = Int32(context.component.model.firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(i)
                    if selectedDays.contains(dayTimestamp) {
                        if var currentSelection = selectionsByLine[lineIndex] {
                            if rowIndex < currentSelection.range.lowerBound {
                                currentSelection.range = rowIndex ... currentSelection.range.upperBound
                                currentSelection.leftTimestamp = dayTimestamp
                            } else {
                                currentSelection.range = currentSelection.range.lowerBound ... rowIndex
                                currentSelection.rightTimestamp = dayTimestamp
                            }
                            selectionsByLine[lineIndex] = currentSelection
                        } else {
                            selectionsByLine[lineIndex] = LineSelection(
                                range: rowIndex ... rowIndex,
                                leftTimestamp: dayTimestamp,
                                rightTimestamp: dayTimestamp
                            )
                        }
                    }
                }
            }

            if let selectedDays = context.component.selectedDays {
                for (lineIndex, selection) in selectionsByLine.sorted(by: { $0.key < $1.key }) {
                    if selection.leftTimestamp == selection.rightTimestamp && selection.leftTimestamp == selectedDays.lowerBound && selection.rightTimestamp == selectedDays.upperBound {
                        continue
                    }
                    let dayEnvironment = context.environment[DayEnvironment.self].value

                    let dayItemSize = updatedDays[0].size
                    let selectionRadius: CGFloat = min(dayItemSize.width, dayItemSize.height)

                    let deltaWidth = floor((weekdayWidth - selectionRadius) / 2.0)
                    let deltaHeight = floor((weekdaySize - selectionRadius) / 2.0)
                    let minX = sideInset + CGFloat(selection.range.lowerBound) * weekdayWidth + deltaWidth
                    let maxX = sideInset + CGFloat(selection.range.upperBound + 1) * weekdayWidth - deltaWidth
                    let minY = baseDayY + CGFloat(lineIndex) * (weekdaySize + weekdaySpacing) + deltaHeight
                    let maxY = minY + selectionRadius

                    let monthSelectionColor = context.component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1)

                    let selectionRect = CGRect(origin: CGPoint(x: minX, y: minY), size: CGSize(width: maxX - minX, height: maxY - minY))
                    let selection = selections[lineIndex].update(
                        component: AnyComponent(Image(image: dayEnvironment.imageCache.monthSelection(leftRadius: selectionRadius, rightRadius: selectionRadius, maxRadius: selectionRadius, color: monthSelectionColor))),
                        availableSize: selectionRect.size,
                        transition: .immediate
                    )
                    let delayIndex = dayEnvironment.selectionDelayCoordination
                    context.add(selection
                        .position(CGPoint(x: selectionRect.midX, y: selectionRect.midY))
                        .appear(ComponentTransition.Appear { _, view, transition in
                            if case .none = transition.animation {
                                return
                            }
                            let delay = Double(min(delayIndex, 6)) * 0.1
                            view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05, delay: delay)
                            view.layer.animateFrame(from: CGRect(origin: view.frame.origin, size: CGSize(width: selectionRadius, height: view.frame.height)), to: view.frame, duration: 0.25, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
                        })
                        .disappear(ComponentTransition.Disappear { view, transition, completion in
                            if case .none = transition.animation {
                                completion()
                                return
                            }
                            view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
                                completion()
                            })
                        })
                    )
                    dayEnvironment.selectionDelayCoordination += 1
                }
            }

            for i in 0 ..< updatedDays.count {
                let gridIndex = gridDayOffset(firstDayOfWeek: context.component.model.firstWeekday, firstWeekdayOfMonth: context.component.model.firstDayWeekday) + i
                let rowIndex = gridIndex % 7
                let lineIndex = gridIndex / 7

                let gridX = sideInset + CGFloat(rowIndex) * weekdayWidth
                let gridY = baseDayY + CGFloat(lineIndex) * (weekdaySize + weekdaySpacing)
                let dayItemSize = updatedDays[i].size
                let dayFrame = CGRect(origin: CGPoint(x: gridX + floor((weekdayWidth - dayItemSize.width) / 2.0), y: gridY + floor((weekdaySize - dayItemSize.height) / 2.0)), size: dayItemSize)
                maxDayY = max(maxDayY, gridY + weekdaySize)
                context.add(updatedDays[i]
                    .position(CGPoint(x: dayFrame.midX, y: dayFrame.midY))
                )
            }

            return CGSize(width: context.availableSize.width, height: maxDayY)
        }
    }
}

private struct DayMedia: Equatable {
    var message: EngineMessage
    var media: EngineMedia

    static func ==(lhs: DayMedia, rhs: DayMedia) -> Bool {
        if lhs.message.id != rhs.message.id {
            return false
        }
        return true
    }
}

private struct MonthModel: Equatable {
    var year: Int
    var index: Int
    var numberOfDays: Int
    var firstDay: Date
    var firstDayWeekday: Int
    var firstWeekday: Int
    var currentYear: Int
    var currentMonth: Int
    var currentDayOfMonth: Int
    var mediaByDay: [Int: DayMedia]

    init(
        year: Int,
        index: Int,
        numberOfDays: Int,
        firstDay: Date,
        firstDayWeekday: Int,
        firstWeekday: Int,
        currentYear: Int,
        currentMonth: Int,
        currentDayOfMonth: Int,
        mediaByDay: [Int: DayMedia]
    ) {
        self.year = year
        self.index = index
        self.numberOfDays = numberOfDays
        self.firstDay = firstDay
        self.firstDayWeekday = firstDayWeekday
        self.firstWeekday = firstWeekday
        self.currentYear = currentYear
        self.currentMonth = currentMonth
        self.currentDayOfMonth = currentDayOfMonth
        self.mediaByDay = mediaByDay
    }
}

private func monthMetadata(calendar: Calendar, for baseDate: Date, currentYear: Int, currentMonth: Int, currentDayOfMonth: Int) -> MonthModel? {
    guard let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: baseDate)?.count, let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate)) else {
        return nil
    }

    let year = calendar.component(.year, from: firstDayOfMonth)
    let month = calendar.component(.month, from: firstDayOfMonth)
    let firstDayWeekday = calendar.component(.weekday, from: firstDayOfMonth)
    let firstWeekday = calendar.firstWeekday

    return MonthModel(
        year: year,
        index: month,
        numberOfDays: numberOfDaysInMonth,
        firstDay: firstDayOfMonth,
        firstDayWeekday: firstDayWeekday,
        firstWeekday: firstWeekday,
        currentYear: currentYear,
        currentMonth: currentMonth,
        currentDayOfMonth: currentDayOfMonth,
        mediaByDay: [:]
    )
}

public final class CalendarMessageScreen: ViewController {
    private final class Node: ViewControllerTracingNode, ASScrollViewDelegate {
        struct SelectionState {
            var dayRange: ClosedRange<Int32>?
        }

        private weak var controller: CalendarMessageScreen?
        private let context: AccountContext
        private let peerId: EnginePeer.Id
        private let initialTimestamp: Int32
        private let enableMessageRangeDeletion: Bool
        private let canNavigateToEmptyDays: Bool
        private let navigateToOffset: (Int, Int32) -> Void
        private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void

        private var presentationData: PresentationData
        private var scrollView: Scroller

        private let calendarSource: SparseMessageCalendar

        private var months: [MonthModel] = []
        private var monthViews: [Int: ComponentHostView<DayEnvironment>] = [:]
        private let contextGestureContainerNode: ContextControllerSourceNode

        private let dayEnvironment: DayEnvironment

        private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
        private var scrollLayout: (width: CGFloat, contentHeight: CGFloat, frames: [Int: CGRect])?

        private var calendarState: SparseMessageCalendar.State?

        private var isLoadingMoreDisposable: Disposable?
        private var stateDisposable: Disposable?

        private weak var currentGestureDayView: DayComponent.View?

        private var selectionToolbarNode: ToolbarNode?
        private(set) var selectionState: SelectionState?

        private var ignoreContentOffset: Bool = false

        init(
            controller: CalendarMessageScreen,
            context: AccountContext,
            peerId: EnginePeer.Id,
            calendarSource: SparseMessageCalendar,
            initialTimestamp: Int32,
            enableMessageRangeDeletion: Bool,
            canNavigateToEmptyDays: Bool,
            navigateToOffset: @escaping (Int, Int32) -> Void,
            previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
        ) {
            self.controller = controller
            self.context = context
            self.peerId = peerId
            self.initialTimestamp = initialTimestamp
            self.enableMessageRangeDeletion = enableMessageRangeDeletion
            self.canNavigateToEmptyDays = canNavigateToEmptyDays
            self.calendarSource = calendarSource
            self.navigateToOffset = navigateToOffset
            self.previewDay = previewDay
            
            self.presentationData = context.sharedContext.currentPresentationData.with { $0 }

            self.contextGestureContainerNode = ContextControllerSourceNode()

            self.scrollView = Scroller()
            self.scrollView.showsVerticalScrollIndicator = true
            self.scrollView.showsHorizontalScrollIndicator = false
            self.scrollView.scrollsToTop = false
            self.scrollView.delaysContentTouches = false
            self.scrollView.canCancelContentTouches = true
            if #available(iOS 11.0, *) {
                self.scrollView.contentInsetAdjustmentBehavior = .never
            }
            self.scrollView.layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
            self.scrollView.disablesInteractiveModalDismiss = true
            if self.presentationData.theme.overallDarkAppearance {
                self.scrollView.indicatorStyle = .white
            } else {
                self.scrollView.indicatorStyle = .black
            }

            self.dayEnvironment = DayEnvironment(imageCache: ImageCache(), directImageCache: DirectMediaImageCache(account: context.account))

            super.init()

            self.contextGestureContainerNode.shouldBegin = { [weak self] point in
                guard let strongSelf = self else {
                    return false
                }

                guard let result = strongSelf.contextGestureContainerNode.view.hitTest(point, with: nil) as? UIButton else {
                    return false
                }

                guard let dayView = result as? DayComponent.View else {
                    return false
                }

                strongSelf.currentGestureDayView = dayView

                return true
            }

            self.contextGestureContainerNode.customActivationProgress = { [weak self] progress, update in
                guard let strongSelf = self, let currentGestureDayView = strongSelf.currentGestureDayView else {
                    return
                }
                let itemLayer = currentGestureDayView.layer

                let targetContentRect = CGRect(origin: CGPoint(), size: itemLayer.bounds.size)

                let scaleSide = itemLayer.bounds.width
                let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
                let currentScale = 1.0 * (1.0 - progress) + minScale * progress

                let originalCenterOffsetX: CGFloat = itemLayer.bounds.width / 2.0 - targetContentRect.midX
                let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale

                let originalCenterOffsetY: CGFloat = itemLayer.bounds.height / 2.0 - targetContentRect.midY
                let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale

                let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
                let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY

                switch update {
                case .update:
                    let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
                    itemLayer.sublayerTransform = sublayerTransform
                case .begin:
                    let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
                    itemLayer.sublayerTransform = sublayerTransform
                case .ended:
                    let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
                    let previousTransform = itemLayer.sublayerTransform
                    itemLayer.sublayerTransform = sublayerTransform

                    itemLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
                }
            }

            self.contextGestureContainerNode.activated = { [weak self] gesture, _ in
                guard let strongSelf = self, let currentGestureDayView = strongSelf.currentGestureDayView else {
                    return
                }
                strongSelf.currentGestureDayView = nil

                currentGestureDayView.isUserInteractionEnabled = false
                currentGestureDayView.isUserInteractionEnabled = true

                if currentGestureDayView.index == nil && !strongSelf.canNavigateToEmptyDays {
                    return
                }
                if let timestamp = currentGestureDayView.timestamp {
                    strongSelf.previewDay(timestamp, currentGestureDayView.index, strongSelf, currentGestureDayView.convert(currentGestureDayView.bounds, to: strongSelf.view), gesture)
                }
            }

            let calendar = Calendar.current

            let baseDate = Date()
            let currentYear = calendar.component(.year, from: baseDate)
            let currentMonth = calendar.component(.month, from: baseDate)
            let currentDayOfMonth = calendar.component(.day, from: baseDate)

            for i in 0 ..< 12 * 20 {
                guard let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate)) else {
                    break
                }
                guard let monthBaseDate = calendar.date(byAdding: .month, value: -i, to: firstDayOfMonth) else {
                    break
                }

                guard let monthModel = monthMetadata(calendar: calendar, for: monthBaseDate, currentYear: currentYear, currentMonth: currentMonth, currentDayOfMonth: currentDayOfMonth) else {
                    break
                }

                let firstDayTimestamp = Int32(monthModel.firstDay.timeIntervalSince1970)
                let lastDayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(monthModel.numberOfDays)

                if let minTimestamp = calendarSource.minTimestamp, minTimestamp > lastDayTimestamp {
                    break
                }

                if monthModel.year < 2013 {
                    break
                }
                if monthModel.year == 2013 {
                    if monthModel.index < 8 {
                        break
                    }
                }

                self.months.append(monthModel)
            }

            self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor

            self.scrollView.delegate = self.wrappedScrollViewDelegate
            self.addSubnode(self.contextGestureContainerNode)
            self.contextGestureContainerNode.view.addSubview(self.scrollView)

            self.isLoadingMoreDisposable = (self.calendarSource.isLoadingMore
            |> distinctUntilChanged
            |> filter { !$0 }
            |> deliverOnMainQueue).startStrict(next: { [weak self] _ in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.calendarSource.loadMore()
            }).strict()

            self.stateDisposable = (self.calendarSource.state
            |> deliverOnMainQueue).startStrict(next: { [weak self] state in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.calendarState = state
                strongSelf.reloadMediaInfo()
            }).strict()
        }

        deinit {
            self.isLoadingMoreDisposable?.dispose()
            self.stateDisposable?.dispose()
        }

        func toggleSelectionMode() {
            var transition: ComponentTransition = .immediate
            if self.selectionState == nil {
                self.selectionState = SelectionState(dayRange: nil)
            } else {
                self.selectionState = nil
                transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))
                transition = transition.withUserData(SelectionTransition.end)
            }

            self.contextGestureContainerNode.isGestureEnabled = self.selectionState == nil
            
            self.updateSelectionState(transition: transition)
        }

        func selectDay(timestamp: Int32) {
            if let selectionState = self.selectionState, selectionState.dayRange == timestamp ... timestamp {
                self.selectionState = SelectionState(dayRange: nil)
            } else {
                self.selectionState = SelectionState(dayRange: timestamp ... timestamp)
            }

            self.contextGestureContainerNode.isGestureEnabled = self.selectionState == nil

            if let (layout, navigationHeight) = self.validLayout {
                self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.5, curve: .spring), componentsTransition: .immediate)
            }
        }

        func openClearHistory(timestamp: Int32) {
            self.selectionState = SelectionState(dayRange: timestamp ... timestamp)
            self.selectionToolbarActionSelected()
        }

        func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition, componentsTransition: ComponentTransition) {
            let isFirstLayout = self.validLayout == nil
            self.validLayout = (layout, navigationHeight)

            var tabBarHeight: CGFloat
            var options: ContainerViewLayoutInsetOptions = []
            if layout.metrics.widthClass == .regular {
                options.insert(.input)
            }
            let bottomInset: CGFloat = layout.insets(options: options).bottom
            if !layout.safeInsets.left.isZero {
                tabBarHeight = 34.0 + bottomInset
            } else {
                tabBarHeight = 49.0 + bottomInset
            }

            let tabBarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))

            if let selectionState = self.selectionState {
                let selectionToolbarNode: ToolbarNode
                let toolbarText: String
                
                var selectedCount = 0
                if let dayRange = selectionState.dayRange {
                    for i in 0 ..< self.months.count {
                        let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970)

                        for day in 0 ..< self.months[i].numberOfDays {
                            let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day)

                            if dayRange.contains(dayTimestamp) {
                                selectedCount += 1
                            }
                        }
                    }
                }
                
                if selectedCount == 0 {
                    toolbarText = self.presentationData.strings.DialogList_ClearHistoryConfirmation
                } else if selectedCount == 1 {
                    toolbarText = self.presentationData.strings.MessageCalendar_ClearHistoryForThisDay
                } else {
                    toolbarText = self.presentationData.strings.MessageCalendar_ClearHistoryForTheseDays
                }
                
                if let currrent = self.selectionToolbarNode {
                    selectionToolbarNode = currrent
                    
                    transition.updateFrame(node: selectionToolbarNode, frame: tabBarFrame)
                    selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: toolbarText, isEnabled: true, color: .custom(self.selectionState?.dayRange != nil ? self.presentationData.theme.list.itemDestructiveColor : self.presentationData.theme.list.itemDisabledTextColor))), transition: transition)
                } else {
                    selectionToolbarNode = ToolbarNode(
                        theme: ToolbarTheme(
                        rootControllerTheme: self.presentationData.theme),
                        displaySeparator: true,
                        left: {
                        },
                        right: {
                        },
                        middle: { [weak self] in
                            self?.selectionToolbarActionSelected()
                        }
                    )
                    selectionToolbarNode.frame = tabBarFrame
                    selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: toolbarText, isEnabled: true, color: .custom(self.selectionState?.dayRange != nil ? self.presentationData.theme.list.itemDestructiveColor : self.presentationData.theme.list.itemDisabledTextColor))), transition: .immediate)
                    self.addSubnode(selectionToolbarNode)
                    self.selectionToolbarNode = selectionToolbarNode
                    transition.animatePositionAdditive(node: selectionToolbarNode, offset: CGPoint(x: 0.0, y: tabBarFrame.height))
                }
            } else if let selectionToolbarNode = self.selectionToolbarNode {
                self.selectionToolbarNode = nil
                transition.updatePosition(node: selectionToolbarNode, position: CGPoint(x: selectionToolbarNode.position.x, y: selectionToolbarNode.position.y + tabBarFrame.height), completion: { [weak selectionToolbarNode] _ in
                    selectionToolbarNode?.removeFromSupernode()
                })
            }

            let _ = self.updateScrollLayoutIfNeeded()

            let previousInset = self.scrollView.contentInset.top
            let updatedInset = self.selectionToolbarNode?.bounds.height ?? 0.0
            if previousInset != updatedInset {
                let delta = updatedInset - previousInset
                self.ignoreContentOffset = true
                let contentOffset = self.scrollView.contentOffset
                self.scrollView.contentInset = UIEdgeInsets(top: updatedInset, left: 0.0, bottom: 0.0, right: 0.0)
                var updatedContentOffset = CGPoint(x: contentOffset.x, y: contentOffset.y - delta)
                if updatedContentOffset.y > self.scrollView.contentSize.height - self.scrollView.bounds.height {
                    updatedContentOffset.y = self.scrollView.contentSize.height - self.scrollView.bounds.height
                }
                if updatedContentOffset.y < -self.scrollView.contentInset.top {
                    updatedContentOffset.y = -self.scrollView.contentInset.top
                }
                self.scrollView.contentOffset = updatedContentOffset
                self.ignoreContentOffset = false
                transition.animateOffsetAdditive(layer: self.scrollView.layer, offset: contentOffset.y - updatedContentOffset.y)
            }

            if isFirstLayout {
                let initialDate = Date(timeIntervalSince1970: TimeInterval(self.initialTimestamp))
                var initialMonthIndex: Int?

                if self.months.count > 1 {
                    for i in 0 ..< self.months.count - 1 {
                        if initialDate >= self.months[i].firstDay {
                            initialMonthIndex = i
                            break
                        }
                    }
                }

                if let initialMonthIndex = initialMonthIndex, let frame = self.scrollLayout?.frames[initialMonthIndex] {
                    var contentOffset = floor(frame.midY - self.scrollView.bounds.height / 2.0)
                    if contentOffset < 0 {
                        contentOffset = 0
                    }
                    if contentOffset > self.scrollView.contentSize.height - self.scrollView.bounds.height {
                        contentOffset = self.scrollView.contentSize.height - self.scrollView.bounds.height
                    }
                    self.ignoreContentOffset = true
                    self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
                    self.ignoreContentOffset = false
                }
            } else {

            }

            updateMonthViews(transition: componentsTransition)
        }

        private func selectionToolbarActionSelected() {
            if self.selectionState?.dayRange == nil {
                if let selectionToolbarNode = self.selectionToolbarNode {
                    let toolbarFrame = selectionToolbarNode.view.convert(selectionToolbarNode.bounds, to: self.view)
                    self.controller?.present(TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: self.presentationData.strings.MessageCalendar_EmptySelectionTooltip), style: .default, icon: .none, location: .point(toolbarFrame.insetBy(dx: 0.0, dy: 10.0), .bottom), shouldDismissOnTouch: { _, _ in
                        return .dismiss(consume: false)
                    }), in: .current)
                }
                
                return
            }
            guard let selectionState = self.selectionState, let dayRange = selectionState.dayRange else {
                return
            }
            var selectedCount = 0
            var minTimestamp: Int32?
            var maxTimestamp: Int32?
            for i in 0 ..< self.months.count {
                let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970)

                for day in 0 ..< self.months[i].numberOfDays {
                    let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day)
                    let nextDayTimestamp = dayTimestamp + 24 * 60 * 60

                    let minDayTimestamp = dayTimestamp
                    let maxDayTimestamp = nextDayTimestamp

                    if dayRange.contains(dayTimestamp) {
                        if let currentMinTimestamp = minTimestamp {
                            minTimestamp = min(minDayTimestamp, currentMinTimestamp)
                        } else {
                            minTimestamp = minDayTimestamp
                        }
                        if let currentMaxTimestamp = maxTimestamp {
                            maxTimestamp = max(maxDayTimestamp, currentMaxTimestamp)
                        } else {
                            maxTimestamp = maxDayTimestamp
                        }
                        selectedCount += 1
                    }
                }
            }

            guard let minTimestampValue = minTimestamp, let maxTimestampValue = maxTimestamp else {
                return
            }

            if selectedCount == 0 {
                return
            }

            enum ClearType {
                case savedMessages
                case secretChat
                case group
                case channel
                case user
            }

            struct ClearInfo {
                var canClearForMyself: ClearType?
                var canClearForEveryone: ClearType?
                var mainPeer: EnginePeer
            }

            let peerId = self.peerId
            if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat {
            } else {
                return
            }
            
            let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
            |> map { chatPeer -> ClearInfo? in
                guard let chatPeer = chatPeer else {
                    return nil
                }

                let canClearForMyself: ClearType?
                let canClearForEveryone: ClearType?

                if peerId == self.context.account.peerId {
                    canClearForMyself = .savedMessages
                    canClearForEveryone = nil
                } else if case .secretChat = chatPeer {
                    canClearForMyself = .secretChat
                    canClearForEveryone = nil
                } else if case let .legacyGroup(group) = chatPeer {
                    switch group.role {
                    case .creator:
                        canClearForMyself = .group
                        canClearForEveryone = nil
                    case .admin, .member:
                        canClearForMyself = .group
                        canClearForEveryone = nil
                    }
                } else if case let .channel(channel) = chatPeer {
                    if channel.hasPermission(.deleteAllMessages) {
                        if case .group = channel.info {
                            canClearForEveryone = .group
                        } else {
                            canClearForEveryone = .channel
                        }
                    } else {
                        canClearForEveryone = nil
                    }
                    canClearForMyself = nil
                } else {
                    canClearForMyself = .user

                    if case let .user(user) = chatPeer, user.botInfo != nil {
                        canClearForEveryone = nil
                    } else {
                        canClearForEveryone = .user
                    }
                }

                return ClearInfo(
                    canClearForMyself: canClearForMyself,
                    canClearForEveryone: canClearForEveryone,
                    mainPeer: chatPeer
                )
            }
            |> deliverOnMainQueue).startStandalone(next: { [weak self] info in
                guard let strongSelf = self, let info = info else {
                    return
                }

                let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
                var items: [ActionSheetItem] = []

                let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
                    guard let strongSelf = self else {
                        return
                    }
                    
                    strongSelf.controller?.completedWithRemoveMessagesInRange?(minTimestampValue ... maxTimestampValue, type, selectedCount, strongSelf.calendarSource)
                    strongSelf.controller?.dismiss(completion: nil)
                }

                if let _ = info.canClearForMyself ?? info.canClearForEveryone {
                    items.append(ActionSheetTextItem(title: strongSelf.presentationData.strings.MessageCalendar_DeleteAlertText(Int32(selectedCount))))

                    if let canClearForEveryone = info.canClearForEveryone {
                        let text: String
                        let confirmationText: String
                        switch canClearForEveryone {
                        case .user:
                            text = strongSelf.presentationData.strings.ChatList_DeleteForEveryone(info.mainPeer.compactDisplayTitle).string
                            confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText
                        default:
                            text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
                            confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText
                        }
                        let _ = confirmationText
                        items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in
                            actionSheet?.dismissAnimated()

                            beginClear(.forEveryone)
                        }))
                    }
                    if let canClearForMyself = info.canClearForMyself {
                        let text: String
                        switch canClearForMyself {
                        case .savedMessages, .secretChat:
                            text = strongSelf.presentationData.strings.Conversation_DeleteManyMessages
                        default:
                            text = strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser
                        }
                        items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in
                            actionSheet?.dismissAnimated()
                            beginClear(.forLocalPeer)
                        }))
                    }
                }

                actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
                    ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
                        actionSheet?.dismissAnimated()
                    })
                ])])

                strongSelf.controller?.present(actionSheet, in: .window(.root))
            })
        }

        func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
            self.contextGestureContainerNode.cancelGesture()
        }

        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if !self.ignoreContentOffset {
                if let indicator = scrollView.value(forKey: "_verticalScrollIndicator") as? UIView {
                    indicator.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)
                }

                self.updateMonthViews(transition: .immediate)
            }
        }

        func updateScrollLayoutIfNeeded() -> Bool {
            guard let (layout, navigationHeight) = self.validLayout else {
                return false
            }
            if self.scrollLayout?.width == layout.size.width {
                return false
            }

            var contentHeight: CGFloat = layout.intrinsicInsets.bottom
            var frames: [Int: CGRect] = [:]

            let measureView = ComponentHostView<DayEnvironment>()
            for i in 0 ..< self.months.count {
                let monthSize = measureView.update(
                    transition: .immediate,
                    component: AnyComponent(MonthComponent(
                        context: self.context,
                        model: self.months[i],
                        foregroundColor: .black,
                        strings: self.presentationData.strings,
                        theme: self.presentationData.theme,
                        dayAction: { _ in
                        },
                        monthAction: { _ in
                        },
                        selectedDays: nil
                    )),
                    environment: {
                        self.dayEnvironment
                    },
                    containerSize: CGSize(width: layout.size.width, height: 10000.0
                ))
                let monthFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: monthSize)
                contentHeight += monthSize.height
                if i != self.months.count {
                    contentHeight += 16.0
                }
                frames[i] = monthFrame
            }

            self.scrollLayout = (layout.size.width, contentHeight, frames)

            self.contextGestureContainerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight))
            self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight))
            self.scrollView.contentSize = CGSize(width: layout.size.width, height: contentHeight)
            self.scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: max(layout.intrinsicInsets.bottom, self.scrollView.contentInset.top), left: 0.0, bottom: 0.0, right: layout.size.width - 3.0 - 6.0)

            return true
        }

        func updateMonthViews(transition: ComponentTransition) {
            guard let (width, _, frames) = self.scrollLayout else {
                return
            }
            
            self.dayEnvironment.selectionDelayCoordination = 0

            let visibleRect = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
            var validMonths = Set<Int>()

            for i in (0 ..< self.months.count).reversed() {
                guard let monthFrame = frames[i] else {
                    continue
                }
                if !visibleRect.intersects(monthFrame) {
                    continue
                }
                validMonths.insert(i)

                var monthTransition = transition
                let monthView: ComponentHostView<DayEnvironment>
                if let current = self.monthViews[i] {
                    monthView = current
                } else {
                    monthTransition = .immediate
                    monthView = ComponentHostView()
                    monthView.layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
                    self.monthViews[i] = monthView
                    self.scrollView.addSubview(monthView)
                }
                let _ = monthView.update(
                    transition: monthTransition,
                    component: AnyComponent(MonthComponent(
                        context: self.context,
                        model: self.months[i],
                        foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor,
                        strings: self.presentationData.strings,
                        theme: self.presentationData.theme,
                        dayAction: { [weak self] timestamp in
                            guard let strongSelf = self else {
                                return
                            }
                            if var selectionState = strongSelf.selectionState {
                                var transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .spring))
                                if let dayRange = selectionState.dayRange {
                                    if dayRange.lowerBound == timestamp || dayRange.upperBound == timestamp {
                                        selectionState.dayRange = nil
                                        transition = transition.withUserData(SelectionTransition.end)
                                    } else if dayRange.lowerBound == dayRange.upperBound {
                                        if timestamp < dayRange.lowerBound {
                                            selectionState.dayRange = timestamp ... dayRange.upperBound
                                        } else {
                                            selectionState.dayRange = dayRange.lowerBound ... timestamp
                                        }
                                        transition = transition.withUserData(SelectionTransition.change)
                                    } else {
                                        selectionState.dayRange = timestamp ... timestamp
                                        transition = transition.withUserData(SelectionTransition.change)
                                    }
                                } else {
                                    selectionState.dayRange = timestamp ... timestamp
                                    transition = transition.withUserData(SelectionTransition.begin)
                                }
                                strongSelf.selectionState = selectionState

                                strongSelf.updateSelectionState(transition: transition)
                            } else if let calendarState = strongSelf.calendarState {
                                outer: for month in strongSelf.months {
                                    let firstDayTimestamp = Int32(month.firstDay.timeIntervalSince1970)

                                    for day in 0 ..< month.numberOfDays {
                                        let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day)
                                        if dayTimestamp == timestamp {
                                            if month.mediaByDay[day] != nil || strongSelf.canNavigateToEmptyDays {
                                                var offset = 0
                                                for key in calendarState.messagesByDay.keys.sorted(by: { $0 > $1 }) {
                                                    if key == dayTimestamp {
                                                        break
                                                    } else if let item = calendarState.messagesByDay[key] {
                                                        offset += item.count
                                                    }
                                                }
                                                strongSelf.navigateToOffset(offset, dayTimestamp)
                                            }

                                            break outer
                                        }
                                    }
                                }
                            }
                        },
                        monthAction: { [weak self] range in
                            guard let strongSelf = self else {
                                return
                            }
                            guard var selectionState = strongSelf.selectionState else {
                                return
                            }
                            var transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .spring))
                            if let dayRange = selectionState.dayRange {
                                if dayRange == range {
                                    selectionState.dayRange = nil
                                    transition = transition.withUserData(SelectionTransition.end)
                                } else {
                                    selectionState.dayRange = range
                                    transition = transition.withUserData(SelectionTransition.change)
                                }
                            } else {
                                selectionState.dayRange = range
                                transition = transition.withUserData(SelectionTransition.begin)
                            }
                            strongSelf.selectionState = selectionState

                            strongSelf.updateSelectionState(transition: transition)
                        },
                        selectedDays: self.selectionState?.dayRange
                    )),
                    environment: {
                        self.dayEnvironment
                    },
                    containerSize: CGSize(width: width, height: 10000.0
                ))
                monthView.frame = monthFrame
            }

            var removeMonths: [Int] = []
            for (index, view) in self.monthViews {
                if !validMonths.contains(index) {
                    view.removeFromSuperview()
                    removeMonths.append(index)
                }
            }
            for index in removeMonths {
                self.monthViews.removeValue(forKey: index)
            }
        }

        private func updateSelectionState(transition: ComponentTransition) {
            var title = self.presentationData.strings.MessageCalendar_Title
            if let selectionState = self.selectionState, let dayRange = selectionState.dayRange {
                var selectedCount = 0
                for i in 0 ..< self.months.count {
                    let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970)

                    for day in 0 ..< self.months[i].numberOfDays {
                        let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day)
                        if dayRange.contains(dayTimestamp) {
                            selectedCount += 1
                        }
                    }
                }

                if selectedCount != 0 {
                    title = self.presentationData.strings.MessageCalendar_DaysSelectedTitle(Int32(selectedCount))
                }
            }

            self.controller?.navigationItem.title = title

            if let (layout, navigationHeight) = self.validLayout {
                self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.5, curve: .spring), componentsTransition: transition)
            }
        }

        private func reloadMediaInfo() {
            guard let calendarState = self.calendarState else {
                return
            }
            var messageMap: [EngineMessage] = []
            for (_, entry) in calendarState.messagesByDay {
                messageMap.append(EngineMessage(entry.message))
            }

            var updatedMedia: [Int: [Int: DayMedia]] = [:]
            for i in 0 ..< self.months.count {
                if updatedMedia[i] == nil {
                    updatedMedia[i] = [:]
                }

                let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970)

                for day in 0 ..< self.months[i].numberOfDays {
                    let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day)
                    let nextDayTimestamp = dayTimestamp + 24 * 60 * 60

                    for message in messageMap {
                        if message.timestamp >= dayTimestamp && message.timestamp < nextDayTimestamp {
                            mediaLoop: for media in message.media {
                                switch media {
                                case _ as TelegramMediaImage, _ as TelegramMediaFile:
                                    updatedMedia[i]![day] = DayMedia(message: message, media: EngineMedia(media))
                                    break mediaLoop
                                default:
                                    break
                                }
                            }

                            break
                        }
                    }
                }
            }
            for (monthIndex, mediaByDay) in updatedMedia {
                self.months[monthIndex].mediaByDay = mediaByDay
            }

            self.updateMonthViews(transition: .immediate)
        }
    }

    private var node: Node {
        return self.displayNode as! Node
    }

    private let context: AccountContext
    private let peerId: EnginePeer.Id
    private let calendarSource: SparseMessageCalendar
    private let initialTimestamp: Int32
    private let enableMessageRangeDeletion: Bool
    private let canNavigateToEmptyDays: Bool
    private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void
    private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void

    private var presentationData: PresentationData
    
    public var completedWithRemoveMessagesInRange: ((ClosedRange<Int32>, InteractiveHistoryClearingType, Int, SparseMessageCalendar) -> Void)?

    public init(
        context: AccountContext,
        peerId: EnginePeer.Id,
        calendarSource: SparseMessageCalendar,
        initialTimestamp: Int32,
        enableMessageRangeDeletion: Bool,
        canNavigateToEmptyDays: Bool,
        navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void,
        previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
    ) {
        self.context = context
        self.peerId = peerId
        self.calendarSource = calendarSource
        self.initialTimestamp = initialTimestamp
        self.enableMessageRangeDeletion = enableMessageRangeDeletion
        self.canNavigateToEmptyDays = canNavigateToEmptyDays
        self.navigateToDay = navigateToDay
        self.previewDay = previewDay

        self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }

        super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))

        self.navigationPresentation = .modal

        self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
        self.navigationItem.setTitle(self.presentationData.strings.MessageCalendar_Title, animated: false)

        if self.enableMessageRangeDeletion {
            if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat {
                self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: false)
            }
        }
    }

    required public init(coder aDecoder: NSCoder) {
        preconditionFailure()
    }

    @objc private func dismissPressed() {
        self.dismiss()
    }

    @objc fileprivate func toggleSelectPressed() {
        if !self.enableMessageRangeDeletion {
            return
        }

        self.node.toggleSelectionMode()

        if self.node.selectionState != nil {
            self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
        } else {
            self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
        }
    }

    public func selectDay(timestamp: Int32) {
        self.node.selectDay(timestamp: timestamp)

        if self.node.selectionState != nil {
            self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
        }
    }

    public func openClearHistory(timestamp: Int32) {
        self.node.openClearHistory(timestamp: timestamp)
    }

    override public func loadDisplayNode() {
        self.displayNode = Node(
            controller: self,
            context: self.context,
            peerId: self.peerId,
            calendarSource: self.calendarSource,
            initialTimestamp: self.initialTimestamp,
            enableMessageRangeDeletion: self.enableMessageRangeDeletion,
            canNavigateToEmptyDays: self.canNavigateToEmptyDays,
            navigateToOffset: { [weak self] index, timestamp in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.navigateToDay(strongSelf, index, timestamp)
            },
            previewDay: self.previewDay
        )

        self.displayNodeDidLoad()
    }

    override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
        super.containerLayoutUpdated(layout, transition: transition)

        self.node.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, componentsTransition: .immediate)
    }
}
